package org.maxgroundstation.world;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.maxgroundstation.world.landmark.Landmark;
import org.maxgroundstation.world.landmark.LandmarkManager;

/**
 * A terrain model created with surface and elevation data from public GIS data portals.
 * 
 * @author Judd Reed
 * 
 * Copyright (C) 2011  Judd Reed
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
public class WorldModel {
	
	// These are used to specify the type of layer to paint	
	private static final String IMPORTED_MAP_IMAGE = "Imported Map";
	private static final String NATURAL_COLOR_IMAGE = "Natural Color";
	private static final String HILL_SHADE_ELEVATION = "Hill Shade";
	private static final String TOPO_LINES_ELEVATION = "Topo Lines";
	private List<String> mapTypeChoices;
	private LandmarkManager landmarkManager;
	
	private void addMapTypeChoice(String layerType) {
		boolean alreadyHaveThisType = false;
		if(layerType.equals("Elevation")) {
			for(String s : mapTypeChoices) {
				alreadyHaveThisType |= s.equals(HILL_SHADE_ELEVATION);
			}
			if(!alreadyHaveThisType) {
				mapTypeChoices.add(HILL_SHADE_ELEVATION);
				mapTypeChoices.add(TOPO_LINES_ELEVATION);
			}
		} else if(layerType.equals("Surface")) {
			for(String s : mapTypeChoices) {
				alreadyHaveThisType |= s.equals(NATURAL_COLOR_IMAGE);
			}
			if(!alreadyHaveThisType) {
				mapTypeChoices.add(NATURAL_COLOR_IMAGE);
			}
		} else if(layerType.equals("Map")) {
			for(String s : mapTypeChoices) {
				alreadyHaveThisType |= s.equals(IMPORTED_MAP_IMAGE);
			}
			if(!alreadyHaveThisType) {
				mapTypeChoices.add(IMPORTED_MAP_IMAGE);
			}
		} else System.out.printf("\n\tUnknown metadata content.\n  How is layer type [%s] to be used?\n\n",
				layerType);
	}
	
	public String getMapTypeChoices() {
		String list = "";
		for(String s : mapTypeChoices) {
			list += s + '\t';
		}
		return list;
	}
	
	public String getMapTypeChoice(int index) {
		return mapTypeChoices.get(index);
	}
	
	// coordinate transform data (defaults replaced by world metadata if present)
	private double refLat = 38.406458;
	private double refLon = -110.791903; 
	private double refUtmEast = 518169;
	private double refUtmNrth = 4250934;
	private double ref_dEdLon = (10000000d / 90d) * Math.cos(refLat*Math.PI/180d);
	private double ref_dEdLat = 0;
	private double ref_dNdLat = 10000000d / 90d;
	private double ref_dNdLon = 0;

	public double getReferenceLatitude(){return refLat;}
	public double getReferenceLongitude(){return refLon;}
	
    private void readReferenceLocation(BufferedReader bReader) throws IOException {
        String buffer = "Read location reference data";
        while(buffer != null) {
            buffer = bReader.readLine();
            if(buffer.startsWith("#"))continue;
            String[] splits = buffer.split("[ \\t]+");
            if(splits[0].equalsIgnoreCase("Latitude")) {
            	refLat = Double.parseDouble(splits[1]);
            	ref_dEdLat = ref_dNdLat = 0;
            	System.out.printf("latitude = %f\n", refLat);
            } else if(splits[0].equalsIgnoreCase("Longitude")) {
            	refLon = Double.parseDouble(splits[1]);
            	ref_dEdLon = ref_dNdLon = 0;
            	System.out.printf("longitude = %f\n", refLon);
            } else if(splits[0].equalsIgnoreCase("Northing")) {
            	refUtmNrth = Double.parseDouble(splits[1]);
            	ref_dNdLat = ref_dNdLon = 0;
                System.out.printf("Northing = %f\n", refUtmNrth);
            } else if(splits[0].equalsIgnoreCase("Easting")) {
            	refUtmEast = Double.parseDouble(splits[1]);
              	ref_dEdLat = ref_dEdLon = 0;
                System.out.printf("Easting = %f\n", refUtmEast);
            } else if(splits[0].equals("endOfLocation")) {
                break;
            } else 
            System.out.printf("IGNORING: %s\n", buffer);
            // TODO: add parsing of coordinate transform vectors
        }
        
        if(ref_dNdLat == 0) {
        	ref_dNdLat = 10000000d / 90d; // assume its 10^7 meters from equator to pole
        }
        if(ref_dEdLon == 0) {
        	ref_dEdLon = ref_dNdLat * Math.cos(refLat*Math.PI/180d); // assume world is a SPHERE
        }
    	ref_dEdLat = ref_dNdLon = 0d; // assume images are north aligned
    }

	
    private int numLayer = 0;    
    private DataLayer[] layer = null;
    
    private DataLayer readLayer(int layerNumber, BufferedReader bReader) {
        DataLayer dl = new DataLayer();
        String buffer = "Read one layer";
        
        System.out.printf("    (%d)\n",layerNumber);
        try {
            while(buffer != null) {
                buffer = bReader.readLine();
                if(buffer.startsWith("#"))continue;
                String[] splits = buffer.split("[ \\t]+");
                System.out.printf("splits = [");for(String s:splits)System.out.printf("(%s)",s);
                System.out.printf("]\n"); System.out.flush();
                if(splits[0].equals("LayerType")) {
                    dl.setTypeIsElevation(splits[1].equals("Elevation"));
                    addMapTypeChoice(splits[1]);
                } else if(splits[0].equals("filename")) {
                    dl.imageFileName = splits[1];
                } else if(splits[0].equals("East")) {
                    dl.dEdX = Double.parseDouble(splits[1]);
                    dl.dEdY = Double.parseDouble(splits[2]);
                    dl.lowEast = Double.parseDouble(splits[3]);
                } else if(splits[0].equals("North")) {
                    dl.dNdX = Double.parseDouble(splits[1]);
                    dl.dNdY = Double.parseDouble(splits[2]);
                    dl.hiNorth = Double.parseDouble(splits[3]);
                } else if(splits[0].equals("verticalBias")) {
                    dl.bias = Double.parseDouble(splits[1]);
                } else if(splits[0].equals("verticalGain")) {
                    dl.gain = Double.parseDouble(splits[1]);
                } else if(splits[0].equals("endOfLayer")) {
                    break;
                } else 
                System.out.printf("readLayer IGNORED: %s\n", buffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return dl;
    }
    
    public WorldModel(String metaDataPath) {
    	mapTypeChoices = new ArrayList<String>();
        File file = new File(metaDataPath);
        //String pathRoot = file.getAbsolutePath().substring(0, file.getAbsolutePath().lastIndexOf('/')+1);
        String pathRoot = metaDataPath.substring(0, metaDataPath.lastIndexOf('/') + 1);
    	//FileReader fReader = null;
    	InputStream stream = null;
        String buffer = "#file contents";
        try {
            //fReader = new FileReader(file);
            stream = getClass().getClassLoader().getResourceAsStream(metaDataPath);
            if (stream != null) {
                BufferedReader bReader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
                while(buffer != null) {
                    buffer = bReader.readLine();
                    if (buffer.startsWith("#")) continue;
                    String[] splits = buffer.split("[ \\t]+");
                    if (splits[0].equals("LayerCount")) {
                        numLayer = Integer.parseInt(splits[1]);
                        System.out.printf("Loading %d scenery data layers\n", numLayer);
                        layer = new DataLayer[numLayer];
                    } else if(splits[0].equals("Location")) {
                        readReferenceLocation(bReader);
                    } else if(splits[0].equals("Layer")) {
                        int layerIndex = Integer.parseInt(splits[1]);
                        layer[layerIndex] = readLayer(layerIndex, bReader);
                    } else if(splits[0].equals("endOfFile")) {
                        break;
                    } else {
                        System.out.printf("IGNORING: %s\n", buffer);
                    }
                }
                bReader.close();
            }
            else {
                System.err.println(metaDataPath + " could not be found.");
            }
        } catch (IOException e) {
            System.err.printf("Error reading from %s\n", metaDataPath);
            e.printStackTrace();
        }
        finally {
            try {
                if (stream != null) stream.close();
            }
            catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
        
        System.out.printf("Metadata describes %d layers:\n",layer.length);
        
        for (int layerNumber = 0; layerNumber < layer.length; layerNumber++) {

            String filePath = pathRoot + layer[layerNumber].imageFileName;
            System.out.printf("layer %d\n", layerNumber);
            System.out.printf("file path = %s\n", filePath);
            System.out.printf("East  = %f * col + %f * row + %f\n", 
                layer[layerNumber].dEdX, layer[layerNumber].dEdY, layer[layerNumber].lowEast);
            System.out.printf("North = %f * col + %f * row + %f\n", 
                layer[layerNumber].dNdX, layer[layerNumber].dNdY, layer[layerNumber].hiNorth);
            
            if(layer[layerNumber].type == DataLayer.LAYER_TYPE.ElevationLayerType) {
                layer[layerNumber].loadElevate(filePath);
            } else {
                layer[layerNumber].loadSurface(filePath);
            }
        }
        
        // Initialize landmark manager.
        landmarkManager = new LandmarkManager();
    }

    /**
     * Performs bilinear interpolation of the elevation image for a specific location.
     * @param easting
     * @param northing
     * @param sampleInterval used to determine sampling alg.
     * @return elevation in meters
     */
    public double getHigh(double easting, double northing, boolean goSlow) {            
        for(DataLayer l: layer) {
            if(l.isElevation()) {
                double fx = ( easting - l.lowEast) / l.dEdX;
                double fy = (northing - l.hiNorth) / l.dNdY;
                
                if((fx<0.0)||(fx>=l.numCol-1)||(fy<0.0)||(fy>=l.numRow-1)) continue;
                
                if(goSlow) {    
                    int ix = (int)fx,   iy = (int)fy;
                    fx -= (double)ix;   fy -= (double)iy;
                    return l.bias + l.gain *
                        ( (double)l.shortPixelArray[(iy+0)*l.numCol+(ix+0)] * (1.0-fy) * (1.0-fx)
                        + (double)l.shortPixelArray[(iy+0)*l.numCol+(ix+1)] * (1.0-fy) * (  fx  )
                        + (double)l.shortPixelArray[(iy+1)*l.numCol+(ix+0)] * (  fy  ) * (1.0-fx)
                        + (double)l.shortPixelArray[(iy+1)*l.numCol+(ix+1)] * (  fy  ) * (  fx  ) );
                } else {
                    // at low res (or far afield), scale back to nearest neighbor for speed
                    int ix = (int)(fx+0.5),     iy = (int)(fy+0.5);
                    return l.bias + l.gain * (double)l.shortPixelArray[iy*l.numCol+ix];
                }
        }   }
        return 0;
    }
    
    public int getSurface(double easting, double northing, boolean goSlow) {
        for(DataLayer l: layer) if(!l.isElevation()) {
            double fx = ( easting - l.lowEast) / l.dEdX;
            double fy = (northing - l.hiNorth) / l.dNdY;
            
            if((fx<0.0)||(fx>=l.numCol-1)||(fy<0.0)||(fy>=l.numRow-1))continue;
            if(goSlow) {
                int ix = (int)fx,   iy = (int)fy;
                fx -= (double)ix;   fy -= (double)iy;
                int[] fourPixelChip = new int[4];
                if(l.redGreenBlueArray != null){
                    fourPixelChip[0] = l.redGreenBlueArray[(iy+0)*l.numCol+ix+0];
                    fourPixelChip[1] = l.redGreenBlueArray[(iy+0)*l.numCol+ix+1];
                    fourPixelChip[2] = l.redGreenBlueArray[(iy+1)*l.numCol+ix+0];
                    fourPixelChip[3] = l.redGreenBlueArray[(iy+1)*l.numCol+ix+1];
                } else {
                    fourPixelChip[0] = l.orthoPhoto.getRGB(ix+0, iy+0);
                    fourPixelChip[1] = l.orthoPhoto.getRGB(ix+1, iy+0);
                    fourPixelChip[2] = l.orthoPhoto.getRGB(ix+0, iy+1);
                    fourPixelChip[3] = l.orthoPhoto.getRGB(ix+1, iy+1);
                }
                int red, grn, blu;
                red = 0x00000ff & (int)
                    ( (float)(fourPixelChip[0]&0x00000ff) * (1.0-fy) * (1.0-fx)
                    + (float)(fourPixelChip[1]&0x00000ff) * (1.0-fy) * (  fx  )
                    + (float)(fourPixelChip[2]&0x00000ff) * (  fy  ) * (1.0-fx)
                    + (float)(fourPixelChip[3]&0x00000ff) * (  fy  ) * (  fx  ) );
                
                grn = 0x000ff00 & (int)
                    ( (float)(fourPixelChip[0]&0x000ff00) * (1.0-fy) * (1.0-fx)
                    + (float)(fourPixelChip[1]&0x000ff00) * (1.0-fy) * (  fx  )
                    + (float)(fourPixelChip[2]&0x000ff00) * (  fy  ) * (1.0-fx)
                    + (float)(fourPixelChip[3]&0x000ff00) * (  fy  ) * (  fx  ) );

                blu = 0x0ff0000 & (int)
                    ( (float)(fourPixelChip[0]&0x0ff0000) * (1.0-fy) * (1.0-fx)
                    + (float)(fourPixelChip[1]&0x0ff0000) * (1.0-fy) * (  fx  )
                    + (float)(fourPixelChip[2]&0x0ff0000) * (  fy  ) * (1.0-fx)
                    + (float)(fourPixelChip[3]&0x0ff0000) * (  fy  ) * (  fx  ) );
                return red | grn | blu; 
            } else {
                int ix = (int)(fx+0.5),     iy = (int)(fy+0.5);
                if(l.redGreenBlueArray != null)
                    return l.redGreenBlueArray[iy*l.numCol+ix];
                else
                    return l.orthoPhoto.getRGB(ix, iy);
            }   
        }
        return 0;
    }   

    private double[] transformGpsToUtm(double latitude, double longitude) {
        double[] xyPair = new double[2];
        xyPair[0] = refUtmEast + ref_dEdLat * (latitude-refLat) + ref_dEdLon * (longitude-refLon);
        xyPair[1] = refUtmNrth + ref_dNdLat * (latitude-refLat) + ref_dNdLon * (longitude-refLon);
        return xyPair;
    }
    
    private final static double TOPO_INTERVAL = 4.0; // elevation change between lines on topo map
  	    
  	public BufferedImage getMapImage(String mapType, double latitude, double longitude, int width,
  	        int height, float pixelsize) {
  	    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
  	    Graphics2D g2D = (Graphics2D) image.getGraphics();
  	    
  	    double[] eastingNorthing = transformGpsToUtm(latitude, longitude);

  		double west = eastingNorthing[0] - (double)width*pixelsize*0.5;
  		double north = eastingNorthing[1] + (double)height*pixelsize*0.5;
  		double east = west + (double)width*pixelsize;
  		double south = north - (double)height*pixelsize;

		if(mapType.equals(TOPO_LINES_ELEVATION)) {
			int x,y;
	
			boolean hold[] = new boolean[height*width];
	    	for(y=0;y<height;y++){
	    		double fN = north + (double)y * (south - north)/(double)height;
	    		for(x=0;x<width;x++){
	    			double fE = west + (double)x * (east - west)/(double)width;
	    			hold[x+width*y] = ((int)(getHigh(fE, fN, true)/TOPO_INTERVAL) % 2)==1;
	    		}
	    	}
	    	/**/
	    	g2D.setColor(Color.WHITE);
            g2D.fillRect(0, 0, width, height);
            
	    	for(y=1;y<height-1;y++){
	    		for(x=1;x<width-1;x++){
	    			int n=0;
	    			for(int r=y-1;r<=y+1;r++) {
		    			for(int c=x-1;c<=x+1;c++) {
		    				n += hold[c+width*r]?1:0;
		    			}	
	    			}
	    			g2D.setColor(((n==0)||(n==9))?Color.WHITE:Color.BLACK);
	    			g2D.drawLine(x, y, x, y);
	    		}	
	    	}
			//g2D.setColor(Color.BLACK);
			//g2D.setBackground(Color.white);
			//g2D.drawString(String.format("SW= %d, %d", (int)south,(int)west), 3, height-4);
			//g2D.drawString(String.format("NE= %d, %d", (int)north,(int)east), width-150, 12);
			
		} else if(mapType.equals(HILL_SHADE_ELEVATION)) {
		   	for(int y=0;y<height;y++){
		   		double fN = north + (double)y * (south - north)/(double)height;
		   		for(int x=0;x<width;x++){
		   			double fE = west + (double)x * (east - west)/(double)width;	    			
	    			double southEastRise =  4.0 + 
	    					getHigh(fE+1.4142d, fN-1.4142d, true) -
	    					getHigh(fE-1.4142d, fN+1.4142d, true);
		    		int ateBits = Math.max(0, Math.min(255, (int)(32.0d*southEastRise)));
		    		g2D.setColor(new Color(0x010101*ateBits));
		    		g2D.drawLine(x, y, x, y);
	    	}	}
			//g2D.setColor(Color.BLACK);
			//g2D.drawString(String.format("SW=%f,%f NE=%f,%f",south,west, north,east), 10, 10);
			
		} else if(mapType.equals(NATURAL_COLOR_IMAGE)) {
			for(int y=0;y<height;y++){
				double fN = north + (double)y * (south - north)/(double)height;
				for(int x=0;x<width;x++){
					double fE = west + (double)x * (east - west)/(double)width;
						
					g2D.setColor(new Color(getSurface(fE,  fN, true)));
					g2D.drawLine(x, y, x, y);
			}	}
			//g2D.setColor(Color.BLACK);
			//g2D.drawString(String.format("SW=%f,%f NE=%f,%f",south,west, north,east), 10, 10);
			
		} else if(mapType.equals(IMPORTED_MAP_IMAGE)) {
			for(DataLayer l: layer) if(l.orthoPhoto != null) { 
				int loXinInput = (int)((west - l.lowEast) / l.dEdX);
				int hiXinInput = (int)((east - l.lowEast) / l.dEdX);
				int loYinInput = (int)((north - l.hiNorth) / l.dNdY);
				int hiYinInput = (int)((south - l.hiNorth) / l.dNdY);
				
				System.out.printf("window into %s = %d,%d to %d,%d\n",
						l.imageFileName,
						loXinInput, loYinInput, hiXinInput, hiYinInput);
				
				if((loXinInput>=0) && (loYinInput>=0)
						&& (hiXinInput<l.numCol) && (hiYinInput<l.numRow)) {
					g2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
						RenderingHints.VALUE_INTERPOLATION_BILINEAR);
					g2D.setRenderingHint(RenderingHints.KEY_DITHERING, 
						RenderingHints.VALUE_DITHER_ENABLE);
					g2D.setRenderingHint(RenderingHints.KEY_RENDERING, 
						RenderingHints.VALUE_RENDER_QUALITY);
					g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
						RenderingHints.VALUE_ANTIALIAS_ON);
					
					g2D.drawImage(l.orthoPhoto, 0, 0, width, height, 
						loXinInput, loYinInput, hiXinInput, hiYinInput, null);
					//g2D.setColor(Color.BLACK);
					//g2D.drawString(String.format("SW=%f,%f NE=%f,%f",south,west, north,east), 10, 10);
					
				}
				else System.out.printf("%s is not an othophoto\n",l.imageFileName);
			}
		   	System.out.println("No Ortho Photo of current Location");
		}
		
		return image;
  	}
  	
  	/**
  	 * Gets a list of landmarks.
  	 * @return list of landmarks.
  	 */
  	public List<Landmark> getLandmarks() {
  	    return landmarkManager.getLandmarks();
  	}
}